package com.google.code.swingonfire.component.page;

import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.LayoutManager;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;

import javax.swing.JComponent;

import com.google.code.swingonfire.animation.Animation;
import com.google.code.swingonfire.animation.AnimationFinishedConsumer;
import com.google.code.swingonfire.animation.AnimationStartingConsumer;
import com.google.code.swingonfire.animation.Animator;
import com.google.code.swingonfire.image.ComponentBufferImageProvider;
import com.google.code.swingonfire.image.ImageProvider;
import com.google.code.swingonfire.utility.Utils;
import com.google.code.swingonfire.utility.interpolation.Interpolation;
import com.google.code.swingonfire.utility.interpolation.Interpolations;

public class JPagedPane extends JComponent implements Animation, AnimationStartingConsumer, AnimationFinishedConsumer,
    ComponentListener
{

    private static final long serialVersionUID = -6964325812274169403L;

    //    private final LRUCache<Integer, ImageProvider> imageProviderCache = new LRUCache<Integer, ImageProvider>(4);

    private JPagedPaneModel model;
    private boolean animationEnabled = true;
    private double animationDuration = 0.5;
    private int selectedComponentIndex;
    private JPageComponentAnimation animation = JPageComponentAnimations.horizontal();
    private Interpolation interpolation = Interpolations.accurate();

    private Integer activeIndex;
    private Component activeComponent;
    private ImageProvider activeImageProvider;

    private Integer incomingIndex;
    private Component incomingComponent;
    private ImageProvider incomingImageProvider;

    private Integer outgoingIndex;
    private Component outgoingComponent;
    private ImageProvider outgoingImageProvider;

    private long animationStartMillis;
    private long animationFinishMillis;
    private double animationStartPageIndex;
    private double animationEndPageIndex;
    private boolean animationRunning = false;

    private double displayedPageIndex;
    private JPagedPaneMovement displayedMovement = JPagedPaneMovement.NONE;
    private double displayedFadeFactor;

    public JPagedPane()
    {
        super();

        super.setLayout(new JPagedPaneLayout());

        addComponentListener(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setLayout(LayoutManager mgr)
    {
        throw new UnsupportedOperationException("Change of layout change of " + getClass() + " prohibited");
    }

    /**
     * Returns the model
     * 
     * @return the model
     */
    public JPagedPaneModel getModel()
    {
        return model;
    }

    /**
     * Sets the model
     * 
     * @param model the model
     */
    public void setModel(JPagedPaneModel model)
    {
        if (!Utils.equals(this.model, model))
        {
            JPagedPaneModel oldModel = this.model;

            this.model = model;

            firePropertyChange("model", oldModel, model);

            if (selectedComponentIndex < model.getMinimumComponentIndex())
            {
                setSelectedComponentIndex(model.getMinimumComponentIndex(), false);
            }

            if (selectedComponentIndex > model.getMaximumComponentIndex())
            {
                setSelectedComponentIndex(model.getMaximumComponentIndex(), false);
            }
        }
    }

    /**
     * Returns the animation
     * 
     * @return the animation
     */
    public JPageComponentAnimation getAnimation()
    {
        return animation;
    }

    /**
     * Sets the animation
     * 
     * @param animation the animation
     */
    public void setAnimation(JPageComponentAnimation animation)
    {
        if (!Utils.equals(this.animation, animation))
        {
            JPageComponentAnimation oldAnimation = this.animation;

            this.animation = animation;

            firePropertyChange("animation", oldAnimation, animation);
        }
    }

    /**
     * Returns the interpolation
     * 
     * @return the interpolation
     */
    public Interpolation getInterpolation()
    {
        return interpolation;
    }

    /**
     * Sets the interpolation
     * 
     * @param interpolation the interpolation
     */
    public void setInterpolation(Interpolation interpolation)
    {
        if (!Utils.equals(this.interpolation, interpolation))
        {
            Interpolation oldInterpolation = this.interpolation;

            this.interpolation = interpolation;

            firePropertyChange("interpolation", oldInterpolation, interpolation);
        }
    }

    protected void setActiveIndex(Integer index)
    {
        if (Utils.equals(index, activeIndex))
        {
            return;
        }

        if (activeComponent != null)
        {
            activeComponent.setVisible(false);

            if ((activeComponent != incomingComponent) && (activeComponent != outgoingComponent))
            {
                super.remove(activeComponent);
            }
        }

        activeIndex = index;

        if (Utils.equals(index, incomingIndex))
        {
            activeComponent = incomingComponent;
            activeImageProvider = incomingImageProvider;

            if (activeComponent != null)
            {
                activeComponent.setVisible(true);
            }

            return;
        }

        if (Utils.equals(index, outgoingIndex))
        {
            activeComponent = outgoingComponent;
            activeImageProvider = outgoingImageProvider;

            if (activeComponent != null)
            {
                activeComponent.setVisible(true);
            }

            return;
        }

        if (index == null)
        {
            activeComponent = null;
            activeImageProvider = null;

            return;
        }

        Component component = getPagedComponent(index);

        if (component != null)
        {
            component.setVisible(true);

            super.add(component);

            activeComponent = component;
            activeImageProvider = new ComponentBufferImageProvider(component, true);
        }
        else
        {
            activeImageProvider = null;
        }

//        revalidate();
    }

    protected void setIncomingIndex(Integer index)
    {
        if (Utils.equals(index, incomingIndex))
        {
            return;
        }

        if ((incomingComponent != null) && (incomingComponent != outgoingComponent)
            && (incomingComponent != activeComponent))
        {
            remove(incomingComponent);
        }

        incomingIndex = index;

        if (Utils.equals(index, activeIndex))
        {
            incomingComponent = activeComponent;
            incomingImageProvider = activeImageProvider;

            return;
        }

        if (Utils.equals(index, outgoingIndex))
        {
            incomingComponent = outgoingComponent;
            incomingImageProvider = outgoingImageProvider;

            return;
        }

        if (index == null)
        {
            incomingComponent = null;
            incomingImageProvider = null;

            return;
        }

        Component component = getPagedComponent(index);

        if (component != null)
        {
            component.setVisible(false);
            super.add(component);

            incomingComponent = component;
            incomingImageProvider = new ComponentBufferImageProvider(component, true);
        }
        else
        {
            incomingImageProvider = null;
        }

//        revalidate();
    }

    protected void setOutgoingIndex(Integer index)
    {
        if (Utils.equals(index, outgoingIndex))
        {
            return;
        }

        if ((outgoingComponent != null) && (outgoingComponent != incomingComponent)
            && (outgoingComponent != activeComponent))
        {
            remove(outgoingComponent);
        }

        outgoingIndex = index;

        if (Utils.equals(index, activeIndex))
        {
            outgoingComponent = activeComponent;
            outgoingImageProvider = activeImageProvider;

            return;
        }

        if (Utils.equals(index, incomingIndex))
        {
            outgoingComponent = incomingComponent;
            outgoingImageProvider = incomingImageProvider;

            return;
        }

        if (index == null)
        {
            outgoingComponent = null;
            outgoingImageProvider = null;

            return;
        }

        Component component = getPagedComponent(index);

        if (component != null)
        {
            component.setVisible(false);
            super.add(component);

            outgoingComponent = component;
            outgoingImageProvider = new ComponentBufferImageProvider(component, true);
        }
        else
        {
            outgoingImageProvider = null;
        }

//        revalidate();
    }

    /**
     * Returns true if the animation is enabled
     * 
     * @return true if the animation is enabled
     */
    public boolean isAnimationEnabled()
    {
        return animationEnabled;
    }

    /**
     * Enabled the animation
     * 
     * @param animationEnabled true to enable the animation
     */
    public void setAnimationEnabled(boolean animationEnabled)
    {
        if (this.animationEnabled != animationEnabled)
        {
            boolean oldAnimationEnabled = this.animationEnabled;

            this.animationEnabled = animationEnabled;

            firePropertyChange("animationEnabled", oldAnimationEnabled, animationEnabled);
        }
    }

    /**
     * Returns the duration for the animation in seconds
     * 
     * @return the duration for the animation in seconds
     */
    public double getAnimationDuration()
    {
        return animationDuration;
    }

    /**
     * Sets the duration of the animation in seconds
     * 
     * @param animationDuration the duration of the animation in seconds
     */
    public void setAnimationDuration(double animationDuration)
    {
        if (this.animationDuration != animationDuration)
        {
            double oldAnimationDuration = this.animationDuration;

            this.animationDuration = animationDuration;

            firePropertyChange("animationDuration", oldAnimationDuration, animationDuration);
        }
    }

    /**
     * Returns the currently selected index
     * 
     * @return the currently selected index
     */
    public int getSelectedComponentIndex()
    {
        return selectedComponentIndex;
    }

    /**
     * Sets the currently selected index and animates the process to display it (if the animation is generally enabled)
     * 
     * @param selectedComponentIndex the currently selected index
     */
    public void setSelectedComponentIndex(int selectedComponentIndex)
    {
        setSelectedComponentIndex(selectedComponentIndex, animationEnabled);
    }

    /**
     * Sets the currently selected index
     * 
     * @param selectedPageIndex the currently selected index
     * @param animationEnabled true to animate, false otherwise
     */
    public void setSelectedComponentIndex(int selectedPageIndex, boolean animationEnabled)
    {
        if (selectedComponentIndex != selectedPageIndex)
        {
            int oldSelectedIndex = selectedComponentIndex;

            selectedComponentIndex = selectedPageIndex;

            firePropertyChange("selectedComponentIndex", oldSelectedIndex, selectedPageIndex);

            animate(animationEnabled);
        }
    }

    /**
     * Starts the animation, if enabled. If the animation is not enabled, it triggers the new index to be visible
     */
    protected void animate(boolean animationEnabled)
    {
        //        Component pagedComponent = getPagedComponent(selectedComponentIndex);

        //        createPageComponentImageProvider(selectedComponentIndex, pagedComponent);
        setActiveIndex(selectedComponentIndex);

        if ((animationEnabled) && (animationDuration > 0))
        {
            long millis = System.currentTimeMillis();

            setAnimationFinishMillis((long) (millis + (animationDuration * 1000)));

            if (!animationRunning)
            {
                animationRunning = true;

                Animator.start(this, this);
            }
        }
        else
        {
            if (animationRunning)
            {
                Animator.stop(this);
            }
            else
            {
                displayedPageIndex = selectedComponentIndex;
                displayedMovement = JPagedPaneMovement.NONE;
            }
        }
    }

    protected long getAnimationFinishMillis()
    {
        return animationFinishMillis;
    }

    protected void setAnimationFinishMillis(long animationFinishMillis)
    {
        this.animationFinishMillis = animationFinishMillis;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void paint(Graphics graphics)
    {
        if (isOpaque())
        {
            graphics.setColor(getBackground());
            graphics.fillRect(0, 0, getWidth(), getHeight());
        }

        if (animationRunning)
        {
            Graphics2D g = (Graphics2D) graphics.create();

            //            System.out.printf("out=%,.3f, in=%,.3f\n", displayedFadeFactor, displayedFadeFactor);

            switch (displayedMovement)
            {
                case STILL:
                    g.drawImage(incomingImageProvider.getImage(), 0, 0, null);
                    break;
                case FORWARD:
                    animation.drawForwardAnimation(g, getWidth(), getHeight(), outgoingImageProvider.getImage(),
                        incomingImageProvider.getImage(), displayedFadeFactor);
                    break;
                case BACKWARD:
                    animation.drawBackwardAnimation(g, getWidth(), getHeight(), outgoingImageProvider.getImage(),
                        incomingImageProvider.getImage(), displayedFadeFactor);
                    break;

                case NONE:
                default:
                    break;

            }

            //animation.renderAnimation((Graphics2D) graphics, getWidth(), getHeight());
        }
        else
        {
            super.paint(graphics);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void animationStarting(Animation animation)
    {
        animationStartMillis = System.currentTimeMillis();
        animationStartPageIndex = displayedPageIndex;
        animationEndPageIndex = selectedComponentIndex;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void animationFinished(Animation animation)
    {
        animationRunning = false;

        displayedPageIndex = selectedComponentIndex;
        displayedMovement = JPagedPaneMovement.NONE;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public double animationStep(double delay, double time, double fraction)
    {
        System.out.println(getComponentCount());
        
        long animationMillis = System.currentTimeMillis() - animationStartMillis;
        long animationDurationMillis = animationFinishMillis - animationStartMillis;
        double factor = Utils.bound(0.0, (double) animationMillis / animationDurationMillis, 1.0);

        if (interpolation != null)
        {
            factor = interpolation.calculate(factor);
        }

        if (animationEndPageIndex != selectedComponentIndex)
        {
            animationStartPageIndex =
                selectedComponentIndex - ((selectedComponentIndex - displayedPageIndex) / (1 - factor));
            animationEndPageIndex = selectedComponentIndex;
        }

        double displayedIndex = animationStartPageIndex + ((selectedComponentIndex - animationStartPageIndex) * factor);

        //        System.err.printf(
        //            "selected = %d, index = %,.3f, factor = %,.3f, delay = %,.3f, time = %,.3f, fraction = %,.3f\n",
        //            selectedIndex, displayedIndex, factor, delay, time, fraction);

        if (Math.abs(displayedIndex - selectedComponentIndex) < 0.001)
        {
            displayedPageIndex = selectedComponentIndex;

            displayedMovement = JPagedPaneMovement.STILL;
            //            outgoingIndex = selectedComponentIndex;
            //            incomingIndex = selectedComponentIndex;
            displayedFadeFactor = 0;
        }
        else if (displayedIndex < selectedComponentIndex)
        {
            displayedPageIndex = displayedIndex;

            displayedMovement = JPagedPaneMovement.FORWARD;
            setOutgoingIndex((int) Math.floor(displayedIndex));
            setIncomingIndex((int) Math.ceil(displayedIndex));
            displayedFadeFactor = displayedIndex - outgoingIndex;
        }
        else
        {
            displayedPageIndex = displayedIndex;

            displayedMovement = JPagedPaneMovement.BACKWARD;
            setOutgoingIndex((int) Math.ceil(displayedIndex));
            setIncomingIndex((int) Math.floor(displayedIndex));
            displayedFadeFactor = 1 - (displayedIndex - incomingIndex);
        }

        repaint();

        return (factor >= 1) ? Double.NaN : 0;
    }

    protected Component getPagedComponent(int index)
    {
        Component pagedComponent = model.getComponent(index);

        if (((JPagedPaneLayout) getLayout()).layoutComponent(this, pagedComponent))
        {
            pagedComponent.doLayout();
        }

        return pagedComponent;
    }

    //    protected ImageProvider getPagedComponentImageProvider(int index)
    //    {
    //        ImageProvider imageProvider = imageProviderCache.get(index);
    //
    //        if (imageProvider != null)
    //        {
    //            return imageProvider;
    //        }
    //
    //        imageProvider = createPageComponentImageProvider(index, getPagedComponent(index));
    //
    //        return imageProvider;
    //    }
    //
    //    private ImageProvider createPageComponentImageProvider(int index, Component component)
    //    {
    //        ImageProvider imageProvider = new ComponentBufferImageProvider(component, true);
    //
    //        imageProviderCache.put(index, imageProvider);
    //
    //        return imageProvider;
    //    }

    @Override
    public void componentResized(ComponentEvent e)
    {
        //        imageProviderCache.clear();
    }

    @Override
    public void componentMoved(ComponentEvent e)
    {
        // intentionally left blank
    }

    @Override
    public void componentShown(ComponentEvent e)
    {
        // intentionally left blank
    }

    @Override
    public void componentHidden(ComponentEvent e)
    {
        //        imageProviderCache.clear();
    }

}
